/**
 * Encrypted Media Extension API
 *    为 web 应用提供了加密媒体和 DRM 代理软件之间提供通信通道（DRM：数字版权管理，用于数字媒体的版权监控和加密保护）
 */

/**
 * navigator.requestMediaKeySystemAccess(string keySystem, obj supportedConfigurations)
 *    返回一个 Promise 携带者一个 MediaKeySystemAccess 对象，该对象提供了访问特定媒体密钥系统的能力，
 * 而这个密钥系统可用于创建解密媒体流的密钥。
 *    这个方法的调用可能会产生一个用户可见的授权提示，申请访问一个或多个系统资源。因此不该在不方便的时候调用该函数。
 *    通常做法是，仅在确定需要 MediaKeys 对象在媒体播放期间解密数据时调用，
 * MediaKeys 对象来自于 MediaKeySystemAccess 对象的 createMediaKeys 方法。
 */


  /**
   * @param {string} keysystem 标记识别一个密钥系统，常常是一个密钥系统域名 或者 org.w3.clearkey
   * @param {Array<MediaKeySystemConfiguration>} supportedConfigurations
   * MediaKeySystemConfiguration 字典： {
   *    label: {string} || ""，保存 MediaKeySystemAccess.getConfiguration() 的返回值
   *    initDataTypes: {Array<string>} || []，保存受支持的初始化数据类型名列表
   *    audioCapabilities: {Array<MediaKeySystemMediaCapability>} || []，受支持的音频类型和功能对列表
   *    videoCapabilities: {Array<MediaKeySystemMediaCapability>} || []，受支持的视频类型和功能对列表
   *    distinctiveIdentifier: {MediKeysRequirement} || 'optional'，标识是否必须使用一个特色标识符
   *    persistentState: {MediKeysRequirement} || 'optional'，是否必须保留状态值
   *    sessionTypes: {Array<MediaKeySessionTypes>} || ["temporary"]，必须被支持的 MediaKeySessionTypes 列表
   * }
   * 
   * 上面又提到了几个数据类型 MediaKeySystemMediaCapability、MediKeysRequirement、MediaKeySessionTypes
   * MediaKeySystemMediaCapability 字典： {
   *    contentType: {string} || ""，媒体资源类型，值必须是可用的媒体 MIME 类型，为空无效
   *    robustness: {string} || ""，和内容类型关联的鲁棒性等级，为空表示可以对任何 contentType 进行解密和解码
   * };
   * enum MediKeysRequirement 字典，： [ 
   *    "required":
   *        在 requestMediaKeySystemAccess 中表示必须支持某个特性
   *        通过 MediaKeySystemAccess 对象返回时表示当前创建的 CDM 实例可以使用此功能
   *    "optional"：
   *        在 requestMediaKeySystemAccess 中表示可能支持某个特性
   *        通过 MediaKeySystemAccess 对象返回时表示该值不能存在于此类对象中
   *    "not-allowed"
   *        在 requestMediaKeySystemAccess 中表示返回的对象一定没有这个能力，任何时候都不能使用
   *        通过 MediaKeySystemAccess 对象返回时表示当前创建的 CDM 实例一定不能使用这个特性
   * ]
   * enum MediaKeySessionTypes 字典： {
   *    "temporary":
   *        一个会话，表示与该会话相关的许可、密钥和记录数据等都不保存。应用系统不需要担心这样的存储管理，这是用户代理必须实现的。
   *    "persistent-license":
   *        一个会话，与该会话相关的许可等数据都保留，当会话关联着的证书销毁时保存一个证书销毁记录，
   *      用于证明用户代理不在需要使用证书、密钥等。这个类型的会话，只能通过在 requestMediaKeySystemAccess 调用时给入配置：
   *      persisitentState: "required"才能生成。这个会话必须在 upload() 调用时通过 Session ID 加载，
   *      当 remove() 后销毁消息发送到 update 函数时生成一条 “license-release" 消息。 
   * }
   * @promise resolve {MediaKeySystemAccess} MediaKeySystemAccess
   * MediaKeySystemAccess 字典： {
   *    keySystem: {string}，标识唯一正在使用的密钥系统
   *    getConfiguration: {Function} 返回 requestMediaKeySystemAccess 方法所选配置对象的支持组合
   *    createMediaKeys: {Function} 为当前密钥系统创建一个新的 MediaKeys 对象，通过 Promise resolve 给出。
   * }
   * 
   * 这里 MediaKeys 又是一个常用接口。
   *    一个 MediaKeys 对象表示一个与 HTMLMediaElement 关联的可在播放中解码媒体数据的密钥的集合。
   * 同时也是表示一个 CDM 实例。
   * MediaKeys 字典： {
   *    createSession: {Function(MediaKeySessionTypes)}，返回一个 MediaKeySession 类型的对象，给入的 MediaKeySessionTypes 会影响结果对象行为 
   *    setServerCertificate: {Function(BufferSource)}，提供一个服务证书，用于许可服务中加密消息，同时密钥系统支持通过消息队列从服务端获取这个证书。返回 Promise<boolea>
   * }
   * 
   * 通过调用 MediaKeys.createSession，生成 MediaKsySession 对象，代表一个密钥会话，当且仅当它的 closed 属性被 resolve 时会话关闭。
   * 用户代理应该在所有没有 closed 的 MediaKeySession 上执行 CDM 状态变更检查算法。该算法必须和主线程并行，且和其他要求并行的行为同步执行。
   * 没有 closed 的 MediaKeySession 应该一直接收事件，否则可以销毁。
   * MediaKeySession 字典：{
   *    sessionid: {string} 只读，CDM 生成的 MediaKeySession 的唯一标识符，同时也是相关联的密钥和许可的唯一标识符
   *    expriation: {double} 只读，当前会话中所有密钥的过期时间，右 CDM 给出，当时间不存在或者许可明确表示没有过期时间时为 NAN。而且在会话周期中可能被某些行为改变
   *    closed: {Promise<void>} 只读，会话关闭算法的运行结果作为 promise resolve 的结果，这个 Promise 只会 resolve。
   *    keyStatuses: {MediaKeyStatusMap} 只读，是{对会话已知的密钥ID : 密钥的当前状态}的映射的引用。
   *    onkeystatuseschange: {EventHandler} keystatuseschange 事件的处理器
   *    onmessage: {EventHandler} message 事件处理器
   *    generateRequest: {Function(string initDataType, BufferSource initData)}，如果执行成功，一个“license-request"或者"individualization-request"消息被放入排队，并 resolve Promise
   *    load: {Function(string sessionId)}，在指定的会话上加载存储的数据到对象上，返回 Promise<void> 给出结果
   *    update: {Function(BufferSource response)}，更新给 CDM 的消息，包括许可证书，返回 Promise<void>
   *    close: {Function} 表明不再需要会话，告诉 CDM 应该释放会话资源并关闭它。要保持的数据不应该释放或者清除，返回 Promise<void>
   * }
   * 
   * 这里遇到了一个新的接口 MediaKeyStatusMap
   *    MedisKeyStatusMap 对象是一个只读的映射，反映密钥ID和关联密钥当前状态的关系，
   * 密钥状态和这个密钥是否用于媒体数据无关。
   * MediaKeyStatusMap 字典： {
   *    size: {unsigned long} 只读，已知密钥的个数
   *    has: {Function(BufferSource keyId)} 通过 keyId 找到对应的密钥，如果该密钥状态已知则返回 true ，否则返回 false。
   *    get: {Function(BufferSource keyId)} 返回 keyId 指定的密钥的 MediaKeyStatus 对象，如果这个密钥的状态是未知的则返回 undefined
   * }
   * 
   * 上面的 get 函数返回的 MediaKeyStatus 是一个枚举值，可用值包括：
   * enum MediaKeyStatus 字典：{
   *    usable: 确认 CDM 的当前密钥可用于解密。只要不确定当前密钥可用于解密就不能使用本值
   *    expired: 密钥已不能用于解密，因为它的过期时间已到
   *    released: 密钥本身已不能用于 CMD，但诸如“许可销毁记录”这样的数据仍然可用。
   *    output-restricted: 对当前无法满足的密钥有输出限制，用该密钥解密的媒体数据可能会被阻止显示。应用应该避免用该密钥解密可能会触发输出限制的流。
   *    output-downscaled: 对当前无法满足的密钥有输出限制，用该密钥解密的媒体数据可能会以低质量输出。实现这一特性是对用户代理来说是可选的，应用程序不应该依赖这一特性来确保连续播放
   *    status-pending: 密钥状态未知且正在确定。
   *    internal-error: 当前 CDM 密钥不能用于解密，因为有一个与其他值无关的错误。应用程序无法操作该值。
   * }
   * 
   * 前文中提到 MediaKeySession 对象中有两个事件，其中 onmessage 是 MediaKeyMessageEvent 的应用。
   * enum MediaKeyMessageType 字典： {
   *    license-request: 这个消息包含了一个许可证书请求的信息
   *    license-renewal: 这个消息包含了一个更新许可证书的信息
   *    license-release: 这个消息包含了一个许可证书销毁记录。
   *    individualization-request: 消息包含应用程序辅助个性化请求。与其他所有消息一样，消息中的每个标识符都必须在每个源和概要信息中唯一，但不能是唯一永久标识
   * }
   * MediaKeyMessageEvent 是一个构造函数
   * MediaKeyMessageEvent(MediaKeyMessageType type [, MediaKeyMessageEventInit eventInitDict])
   * 生成的实例的字典如下： {
   *    messageType: 只读，MediaKeyMessageType 支持的值，表示消息的类型。
   *    message: 只读，来自 CDM 的消息，可用的缓冲数据。
   * }
   * 
   * 上面 MediaKeyMessageEvent 构造函数用到的可选参数 MediaKeyMessageEventInit 实例，与构造结果一样
   * MediaKeyMessageEventInit 字典： {
   *    messageType: 消息类型
   *    message: 消息实例
   * }
   * 
   * 还没完，当用户代理支持 EME 时，往往对 HTMLMediaElement 进行了扩展，增加以下几个字段：
   *    媒体密钥：默认 false
   *    加密阻塞队列：等待解密的加密数据队列，默认为空队列
   *    解密等待标识：默认 false，
   *    播放等待标识：默认 false
   * 上面的字段在加载媒体资源和 seeking 时应该被重置。
   * 额外的属性和方法： {
   *    mediaKeys: MediaKesy 类型，只读，可为空，解密媒体数据时用的媒体密钥
   *    onencrypted: 事件句柄，encrypted 事件触发
   *    onwaitingforkey: 事件句柄，waitingforkey 事件触发
   *    setMediaKeys: {Function(MediaKeys)} 提供并设置一个 MediaKeys 对象，用于播放时解密，返回一个 Promise<void>
   * }
   * 
   * 事实上还有一个被众多用户代理支持的基础设施 CKS(Common Key System)，它描述了各大密钥系统必须广泛支持的基础能力，同时定义了证书相关的格式。
   * Clear Key 密钥系统即”org.w3.clearkey"，使用清晰的纯文本密钥解密资源，无需额外的客户侧支持。它仅仅支持上文中的部分能力，可以在 w3c 规格文档中查看。
   * 本文这里的将目光聚集于 “证书请求格式化”。
   * 证书请求参数 JSON 对象如下： {
   *    kids: 一个数组，每个成员是包含 Key Id 值的8进制序列，且经过 base64url 编码
   *    type: 请求的 MediaKeySessionType
   * }
   * 下面是 为两个密钥ID请求证书的实例
   * {
   *    "kids": ["LwVHf8JLtPrv2GUXFW2v_A", "0DdtU9od-Bh5L3xbv0Xf_A"],
   *    "type": "temporary"
   * }
   * 
   * 通过 update() 响应参数提供的证书格式，通常是一个 JWK ，包括下面几个成员：
   *    kty: 8进制序列，密钥类型
   *    k: 密钥值，经过 base64url 编码的8进制的对称密钥
   *    kid：key ID，经过 base64url 编码的8进制id值
   * 实例：
   * {
   *    "keys": [{"kty":"oct", "k":"tQ0bJVWb6b0KPL6KtZIy_A", "kid": "LwVHf8JLtPrv2GUXFW2v_A"}],
   *    "type": "temporary"
   * }
   * 
   * 还有 证书释放格式、证书释放确认格式 相似只有一个 kids 数组。
   */

// 下面是一些完整的简单EME使用示例

if(navigator.requestMediaKeySystemAccess) {

  // 已知资源和密钥情形
  function SourceAndKeyKnown () {
    let video = document.getElementById('video');
    if(!video.mediaKeys) {
      navigator.requestMediaKeySystemAccess('org.w3.clearkey', [
        {
          initDataTypes: ['webm'],
          videoCapabilities: [{
            contentType: 'vide/webm; codes=vp8'
          }]
        }
      ]).then(keySystemAccess => {
          let promise = keySystemAccess.createMediaKeys();
          promise.catch(e => {
            console.error.bind(console, 'Unable to create MediaKeys');
            console.error(e);
          });
          
          promise.then(createdMediaKeys => {
            return video.setMediaKeys(createdMediaKeys);
          }).catch(e => {
            console.error.bind(console, 'Unable to set MediaKeys')
            console.error(e);
          });
            
          promise.then(createMediaKeys => {
            let te = new TextEncoder();
            let initData = te.encode(JSON.stringify({
              kids: ['LwVHf8JLtPrv2GUXFW2v_A']
            }));
            let keySession = createdMediaKeys.createSession();
            keySession.addEventListener('message', handleMessage, false);
            return keySession.generateRequest('keyids', initData);
          }).catch(e => {
            console.error.bind(console, 'Unable to create or initialize key session');
            console.error(e);
          });
      }).catch(e => {
        console.error.bind(console, 'key system access')
        console.error(e);
      });
    }

    // keySession 的 message 对象处理
    function handleMessage (event) {
      console.log(event);
      let keySession = event.target;
      let te = new TextEncoder();
      let license = te.encode(JSON.stringify({
        keys: [{
          kty: 'oct',
          k: 'tQ0bJVWb6b0KPL6KtZIy_A',
          kid: 'LwVHf8JLtPrv2GUXFW2v_A'
        }],
        type: 'temporary'
      }));
      keySession.update(license).catch(
        console.error.bind(console, 'update() failed')
      );
    }
  }

} else {
  alert('您的浏览器不支持 EME');
}